Skip to content

Read the clause (c) TAIL case on a checkout at a short fixed path, and drive lane-start's row stamp at three checkout lengths, so the case no longer answers by the length of the run root (#180) - #215

Merged
brettheap merged 5 commits into
mainfrom
fix/row-stamp-tail-case-180
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #180

What was wrong

tests/test_lane_helpers.sh's clause (c) TAIL case asserted that repoA11b-1's row stamp ends dir $HOME/projects/repoA11b; window a11start:3 @42. lane-start's stamp is ONE line under ratified decision O1's 240-character cap (Amendment 13(a)). cut_to_line cuts it at 230 and drops the last partial word. The tail is the checkout path, so the case only passed while $HOME was short. Under plain pytest's /tmp it was green. Under tests/run.sh's per-run root (TMPDIR about 71 characters, so $HOME about 91) the line passed 230, and the cut landed on the ; right after the path, so the path went whole: the row read … window renamed, no launch; dir .... On macOS's runner the case was at the edge.

Reproduced outside the shell suite with a real lane-start, started twice --no-launch, under a 92-character $HOME:

  • checkout under $HOME (107 characters): … window renamed, no launch; dir <107 chars>; .... The window is gone and the head is intact.
  • the same $HOME with the checkout passed as --dir /tmp/ort-tail.XXXXXXXX/repoZ (28 characters): … no launch; dir /tmp/ort-tail.…/repoZ; window testsess:0 @1, 197 characters in all.

Why (A), the test-side fix, and not (B)

The cut is behaving as designed. Nothing in the stamp is broken under long paths.

  • The design puts dir and window last on purpose. lane-start's Amendment 13(a) comment above the stamp (:3668-3677) orders the tail launch first, then dir and window, so "a line long enough to be cut loses what a reader can look up rather than what they cannot". Both facts are sub-fields of the same act's STARTED/RESUMED line in the lane's log, which lane-dir reads and the new test asserts in full. Both are also columns of Amendment 11's listing. No reader parses dir out of the state cell (checked with grep across lane-start, lane-end, lanes-edit.sh, lane, lanes, lane-rename, lane-worktrees, link-estates).
  • The cut is already routine in production. Any launching resume (launching claude --name <lane> --resume <uuid>) is past 230 before dir even starts. So "the stamp drops dir/window under a long line" is today's normal behaviour, not an edge case of long paths.
  • (B) would write a non-path after dir. Today a cut that lands inside the path drops it whole (dir ...), so the row never carries a truncated path that reads as a real one. (B)'s "truncate the dir, keep the facts" would put an elided path there. That changes the stamp format of a governed artefact, which is for the protocol to decide, not for a test fix.
  • (B) alone cannot fix this case. The case asserts the FULL dir $HOME/…, which cannot fit under the cap at run.sh's length whatever the stamp does.
  • cut_to_line is pinned. test_the_three_row_writers_carry_the_same_one_line_cut_byte_for_byte pins it byte for byte across lane-start, lane-end and lane-handoff.

So the case reads the tail on a checkout whose path has the same length wherever the suite runs. No product code changes. tests/run.sh is untouched: lane 1's #195 edits it.

What changed

tests/test_lane_helpers.sh (edited in place, never at the end; ranges are at this head):

  • :66-67: A11T_ROOT="" beside SANDBOX=, before the traps, so cleanup never acts on an inherited value.
  • cleanup() :101-103: removes $A11T_ROOT. It runs on EXIT, INT and TERM. Open PR A silent tmux is no longer read as a dead window, the companion verdict is read off the lane's live window from any window, and the guard's cure for a lane named before Rule 4 says what it cannot fill in (#73) #222 (at 2946a41) also adds a line to cleanup() here, so the two conflict in this one hunk of tests/test_lane_helpers.sh (~:101). The resolution is to keep both lines, and it falls to whichever PR lands second.
  • clause (c) block :5356-5389: replaces the old :5351-5356.
    • repoA11b-1's second start keeps its exit, , no launch and head assertions. These do not depend on the root. One description now reads "…and its stamp says what actually happened" because its antecedent moved.
    • The TAIL assertion moves to a new lane repoA11t-1, whose checkout is mktemp -d <base>/ort-a11t.XXXXXX + /repoA11t. <base> is the sandbox's own $TMPDIR when that is at most 40 characters (plain pytest on Linux, where the checkout then lives inside $SANDBOX), and /tmp when it is not (run.sh, macOS). So the checkout is at most 65 characters, an assertion holds it there, and the line stays under about 210 wherever the root is. It runs in its own fake window a11tail:5 @43, which nothing else in the suite uses.
    • Only a SIGKILLed run under a long root leaves an ort-a11t.* behind, and it is in /tmp, outside run.sh's per-run root. Any other exit is cleaned up by cleanup.
    • The lane is started with --dir, then started again, and the second start reads the checkout back from the lane's log, the same shape as before.
    • repoA11b-1 keeps $HOME/projects/repoA11b as its recorded directory, because the later DIRECTORY PRECEDENCE dry-run (:5668) reads it back.

tests/test_lane_start_row_stamp.py (new): three parametrized cases, each a real lane-start started twice --no-launch in a sandbox whose $HOME is at least run.sh's length. The checkout is at an exact length under a /tmp mkdtemp, so the answer does not depend on pytest's own basetemp. The workspace seed is a data table written in a loop, which also clears SonarCloud's duplication gate against tests/test_lane_start_claude_current.py:

All three also assert the head, the 240 cap, and the lane log's full dir …; window ….

Tests

  • Local, under the shared lock (flock -w 1800 ${TMPDIR:-/tmp}/openrepotools-pytest.lock, lock taken 20:40:57Z, at the fix round's tree):
  • CI at 085a234 (before this fix round): the Linux tests job passed, 1422 passed and 9 skipped. Two sibling PRs on the same base that add no pytest cases show 1419 passed and 9 skipped, so the difference is the three new cases. The shell suite's wrapper passed too. Every other job passed, except tests-macos, which waits for the ready label.
  • SonarCloud at 085a234 failed its quality gate on 4.7% new-code duplication: the register seed, :164-177, against tests/test_lane_start_claude_current.py:198-211. The seed is restructured at this head.

🤖 Generated with Claude Code

Summary by Sourcery

Stabilize lane row-stamp coverage by testing checkout paths at controlled lengths instead of relying on the surrounding run root.

Bug Fixes:

  • Make the clause (c) TAIL test independent of the run-root and home-directory path length by using a short fixed checkout path.
  • Add coverage for row-stamp behavior across short, boundary-length, and long checkout paths, verifying paths are dropped whole rather than truncated.

Enhancements:

  • Preserve validation of the complete directory and window details in the lane log while testing the capped row stamp.

Tests:

  • Add parametrized integration tests that start lanes twice with real checkout paths and verify row-stamp caps, preserved heads, and path-dependent tail cuts.

Brett Heap and others added 2 commits October 13, 2026 18:25
… is a measured design and not a run root's accident

The stamp's tail (Amendment 11(c)'s dir and window) is what cut_to_line
takes first under ratified decision O1's cap; its length is the checkout
path's, not the run root's. A real lane-start, started twice --no-launch
under a $HOME at least as long as tests/run.sh's, with the checkout at a
short fixed path (tail intact), at 100 characters (window lost, #180's
shape) and at 160 (the path dropped whole, never truncated); the head,
the 240 cap and the log's full facts are asserted in all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its answer no longer depends on the run root (#180)

Under $HOME/projects the stamp's line outgrew the 230 the cut keeps once
the run root was tests/run.sh's, and the window ref was cut: red locally,
green under CI's plain pytest. The tail is now read on a lane whose
checkout is /tmp/ort-a11t.XXXXXX/repoA11t (29 characters anywhere);
repoA11b-1's second start keeps its exit, no-launch and head assertions,
and cleanup removes the one fixture outside $SANDBOX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 22 hours and 56 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes flaky clause (c) TAIL coverage by decoupling the full-tail assertion from variable $HOME/run-root lengths, without changing product code, and adds isolated parametrized tests that document and verify how lane-start stamps are cut at short, boundary, and long checkout paths.

File-Level Changes

Change Details Files
Make the shell-suite TAIL assertion independent of the run-root length by using a short, fixed checkout path.
  • Move the full dir/window TAIL assertion to a dedicated checkout under /tmp.
  • Start the replacement lane twice so the second invocation reads the checkout from its log, preserving the original scenario.
  • Add cleanup for the fixture created outside the suite sandbox while retaining the existing assertions on the original lane’s head and launch status.
tests/test_lane_helpers.sh
Add deterministic coverage for row-stamp truncation behavior across checkout path lengths.
  • Build an isolated workstation, register, fake tmux, and Git environment for real lane-start executions.
  • Exercise short, 100-character, and 160-character checkout paths with two --no-launch starts.
  • Verify the 240-character cap, intact stamp head, expected tail retention or whole-path omission, and full dir/window facts in the lane log.
tests/test_lane_start_row_stamp.py

Assessment against linked issues

Issue Objective Addressed Explanation
#180 Make the clause (c) TAIL case pass independently of the length of the test run root by reading the row stamp on a checkout at a short, fixed path. ✅
#180 Exercise lane-start’s row-stamp behavior at checkout lengths that fit, partially exceed, and exceed the stamp’s available space, verifying the head remains intact and the directory is not misleadingly truncated. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Brett Heap and others added 2 commits October 13, 2026 20:42
…ases by what the cut takes

SonarCloud counted the register seed as a 14-line copy of
test_lane_start_claude_current.py's (4.7% new-code duplication against
a 3% gate); it is now one table written in a loop, the register text
byte-identical. The 100-character case was labelled #180's shape, but
#180's row read `... no launch; dir ...`, the path dropped whole: the
ids are now window-cut-dir-kept and path-dropped-whole-as-in-180, and
the docstring says which is which.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kout in the sandbox wherever its TMPDIR is short

An inherited A11T_ROOT would have been removed by cleanup had the suite
died before the clause (c) case. The checkout now goes under the
sandbox's own $TMPDIR when that is at most 40 characters, and under
/tmp only when it is not, with an assertion that it is at most 65
characters, so the line stays under about 210 wherever the root is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tamp test's docstring

Since c19150f the clause (c) case's checkout goes under the sandbox's
own TMPDIR when that is short and under /tmp when it is not, so its
length is bounded at 65 characters rather than fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap brettheap added ready landing gate: runs tests-macos once before the squash and removed ready landing gate: runs tests-macos once before the squash labels Oct 17, 2026
@brettheap
brettheap merged commit 61a7378 into main Oct 18, 2026
14 checks passed
@brettheap
brettheap deleted the fix/row-stamp-tail-case-180 branch October 18, 2026 01:58
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…so CI tests this PR on today's main

Main moved through tests/test_lane_helpers.sh (#215) after the last green run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Brings in d424b64 (#201), 61a7378 (#215, the lane-start clause (c)
TAIL fix), 19c2e25 (#232, tests/run.sh's lock owner), f685866 (#233,
the openRepoShape pin at v1.1.0, 190133d) and f595e44 (#229 item 22,
the hygiene rule on bench-root host paths) on top of 83d2690.

Both sides change lanes-edit.sh and docs/README-lanes.md, in disjoint
hunks: main's are `swapped`'s row spelling (Amendment 15) and its
manual paragraph, the branch's are project-legs, lane-reconcile's leg
and paired-root reads and the T018 section. git merged both with no
conflict. bash -n passes on lanes-edit.sh, lane-handoff, lane-start,
lane-end, tests/run.sh and tests/test_lane_helpers.sh.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…58, so CI tests this PR on today's main

The branch last merged aaa7bc8; main has since landed #195, #229, the openRepoShape v1.1.0 pin, and #201/#215/#203 in files this PR shares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… rule)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…e_passes: two #180 row-stamp cuts, main's own); selection triad_lane_tooling or repo_hygiene or test_the_lane_helper_suite_passes

Log: briefs/t018-verify-20261018T034203Z.log (lane openRepoTools-3's
briefs directory), through briefs/verify.sh and tests/run.sh, with every
LANES_* and TMUX* variable unset.
  start 2026-10-18T03:42:03Z head 88ee9d9 tree-of-index 07fa62e
  1 failed, 224 passed, 1 deselected in 4045.64s (1:07:25)
  exit=1 end 2026-10-18T05:34:13Z
The run queued behind other lanes' pytest from 03:42Z and took the
lock at about 04:26Z.

Every test of tests/test_triad_lane_tooling.py,
tests/test_triad_lane_tooling_handoff.py and tests/test_repo_hygiene.py
passed, both goldens unregenerated among them.

The one failure, tests/test_lane_helpers_suite.py::
test_the_lane_helper_suite_passes: the shell suite ended "4620 passed,
2 failed, 0 skipped, 0 pending". Both FAILs are lane-start row-stamp
tail cases of #180's group:
- log line 100, "...with clause (c)'s directory and window in the
  tail, for the person reading the row" (repoA): the cell line is cut
  to "...home/projects/repoA; ..." (228 chars), so "; window " is gone;
- log line 1967, "...having found the checkout by its real directory
  name, not the typed one" (repoCase): the cell line is cut to
  "...no launch; dir ..." (123 chars), the whole path dropped.
Cause: lane-start's cut_to_line caps the state line at 230 characters,
cuts there and trims back to the last space, then appends " ...". The
"; dir <DIR>; window <ref>" tail comes last, and here <DIR> is about 110
characters (the run root under ~/.local/state/openRepoTools/tmp/...), so
the tail is cut: in repoA after the path, in repoCase inside it, where
trimming to the last space drops the path whole. Both "got" strings are
exactly that cut.
Not T018's: `git diff origin/main...HEAD -- lane-start
tests/test_lane_helpers.sh tests/test_lane_helpers_suite.py` is empty,
and T018's lanes-edit.sh hunks (usage, project_legs/assembly_legs,
lane_reconcile, a comment on lane_worktree_roots, the guard exemption,
the project-legs arm and the refusal list) touch neither set-row-state
nor the stamp. Lane 2 measured the same group failing on main's own tree
at a TMPDIR length of 86 (#186 comment 6244714538: repoA, repoA11b,
repoCase and repoMix); #215 (61a7378) moved only repoA11b to a short
fixed path, and repoA11b passed here. CI's tests job, with a short run
root, passes this test on main (83d2690: 1498 passed, 19 skipped;
f5c3bcd and 2cb14a9: success). Not re-run on main locally: the two cases
run only inside the hour-long shell suite.

This run covers 9f23a6b and bf2a220 (the unreadable-leg reads) and this
round: the S1, S2 and N1-N4 fixes (5d2c1c8), lane 1's rulings in §7
(9a8f175), measurement finding 1 (a4e0b17), the bench-root respelling
(a60cf1c), and the merges of main 2732d59, 83d2690, f595e44 (the
openRepoShape pin at v1.1.0) and f5c3bcd (cf7db4a, 107958c, 69f0017,
88ee9d9). The earlier run on 107958c (t018-verify-20261018T011535Z.log)
was killed at a session exit after one F (first in order:
test_the_lane_helper_suite_passes) and 176 passes, with no summary.

Correction to a60cf1c's message: the first of the seven respelled
lines is in §1 (Scope and method), not §0.

Main has since moved to 2cb14a9 and a41adb6; this head does not
include them.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… this PR on today's main

One conflict, in tests/test_lane_helpers.sh's `cleanup` trap: #215 (61a7378) added a removal of `$A11T_ROOT`, #180's short checkout root for the clause (c) TAIL case that lives outside `$SANDBOX`, at the same place this PR added the kill of `$TH73_PID`, the live sleep its window-twin case records. Both are kept: the `TH73_PID` kill stays with the other fixture-process kills, and #215's comment and `A11T_ROOT` removal follow it, ahead of the `$SANDBOX` removal. They do not interact, and the resolved file is exactly main's version plus this PR's own change to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 19, 2026
…es, so the sweep, lane-end's gate, lane-handoff, lane-reconcile and the daily report can see a lane's work there, and change nothing a checkout with no project.yaml does (#246)

* docs: the triad split's lane-tooling side, surface by surface (T006, T007, T009, T010, T018)

docs/triad-lane-tooling-analysis.md: for every lane tool, the installer and
its receipt, the hooks, the manuals, the tests, CI and the brett-wip
register/inventory/handoff data, what the assembly/spec/code split of
opensoft/openRepoTools changes, at file:line on c4864ac, with the plan task
each change belongs to and whether it is a patch or a proposal.

No shipped command changes: no lane tool reads docs/, openspec/ or specs/
at run time (the adoption plan's cross-leg follow-ups for them are comments,
usage text, a URL or the object-key grammar), and every fix the triad does
need changes today's behaviour, so each is a proposal with a sketch. The
suite's test roots (T009/T010) are lane 1's PR #190 (c139e88); this branch's
earlier draft of them was withdrawn before commit, and the document records
the overlap, one gap and one measured defect in #190 for lane 1.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP (untested): T018 worktree visibility for triad legs — the legs reader, the sweep's leg and paired-tree discovery, lane-end's leg rows

Checkpoint for an account swap, on the coordinator's word. NOTHING HERE HAS
BEEN RUN UNDER tests/run.sh: `bash -n` passes for lanes-edit.sh and lane-end,
and lane-worktrees parses as Python; one manual run of
`lanes-edit.sh project-legs` against openRepoShape's assembly-root
project.yaml template (pinned submodule) printed the spec and code legs, exit
0, with no workspace configured; 8 for a directory with no manifest; 64 for a
relative path.

DONE (T018, plan proposal 2 of docs/triad-lane-tooling-analysis.md §4.4):
- lanes-edit.sh: `project_leg_paths_file` (lane-start rung 5's awk, the
  seven deciding lines verbatim, printing every leg) and `assembly_legs`; the
  read-only subcommand `project-legs <checkout>` (0 rows, 8 none, 1 manifest
  unreadable, 64 usage), exempt from the workspace guard; listed in the
  unknown-subcommand refusal and the usage header.
- lane-worktrees sweep: where the lane's dir has a project.yaml naming legs,
  `Leg` rows for each leg and each initialized nested submodule (porcelain
  kind `leg`, a LEGS section, a summary suffix), counted only where the leg's
  HEAD's own Lane: trailer is this lane's and it holds work, or unreadable;
  every leg's worktree registrations enumerated; `<assembly>/worktrees`
  scanned as the paired root and claimed by the same HEAD-trailer rule as
  .claude/worktrees; legs' branches under --branches and probed under
  --fetch. No project.yaml: no helper call and no new code path's output.
- lane-end: `leg` porcelain rows counted (retire) or listed (not counted),
  "leg checkout(s)" in the gate's tally, no `--yes` offered when legs are all
  that is counted.

REMAINS, in order: (1) tests/test_triad_lane_tooling.py — assembly fixture
from the pinned template bytes with legs as real repositories and bare local
remotes, fake gh exiting 1; single-repository golden comparisons against
c45a452's tools (CI checks out depth 1, so the old bytes need a decision:
skip where c45a452 is absent, or captured goldens); the shared-awk identity
check against lane-start; (2) README-lanes.md and usage text for (a);
(3) commit (b) lane-handoff paired-root poll + recording the leg checkout,
lane-reconcile's leg registrations; (4) (c) the daily report's legs;
(5) (d) add recording the leg; (6) the shell suite once at the end.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 (a): the triad guidance for the sweep and lane-end's gate — README-lanes, usage text, the dated §4.4 note; 2aa8a59's code now tested

Plan task T018 (#186), slice (d2), part (a). This
commit carries the guidance for the code 2aa8a59 added: a "A triad: its
legs, their worktrees and the paired trees (T018)" section and a `leg` row
in the porcelain block of docs/README-lanes.md; a "A TRIAD" paragraph in
`lane-worktrees --help`; four lines in lane-end's CLOSE-OUT GATE header
(`lane-end --help`); and a dated subsection at the end of §4.4 of
docs/triad-lane-tooling-analysis.md, including the note "outputs that change
regardless of shape" the coordinator asked for (the unknown-subcommand list
and usage header of lanes-edit.sh, and the --help texts), for lane 1's ruling.

THE INVARIANT: a checkout with no project.yaml declaring a leg other than
itself is asked nothing, so a single repository's sweep rows, gate verdict,
exit codes, register lines and inventory rows are unchanged. Only usage text
moves for every shape, as listed in the §4.4 note.

THE PROOF, run under tests/run.sh with this tree (tools as committed here and
in 2aa8a59):
  tests/run.sh -k "t018 or repo_hygiene or test_lane_worktrees"
  -> 1 failed, 299 passed, 1006 deselected in 1002.77s
The 299 include every test of tests/test_lane_worktrees.py and
tests/test_lane_worktrees_report.py, the hygiene module, and the T018 tests:
the single-repository golden comparison against c45a452's stored outputs and
the assembly cases (template bytes, legs as real repositories behind bare
local origins, fake gh exiting 1). The one failure was the hygiene check on
host-absolute paths, tripped by the golden fixture's `<ROOT>/home/projects/`;
that fixture and its test module are the next commit, WIP, with the fix.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP (untested): T018 (a) tests — tests/test_triad_lane_tooling.py and the single-repository golden captured from c45a452's tools

Checkpoint for an account swap, on the coordinator's word. The module and
its stored fixture, after the fix of the one failure the previous run found;
the fixed form has NOT been run under tests/run.sh (the re-run,
`tests/run.sh -k "t018 or repo_hygiene"`, was stopped while still waiting
before the lock, exit 143, so it never held it).

DONE:
- The stored-fixture decision: the "before" of the single-repository
  invariant is tests/fixtures/t018/single-repository.golden, generated ONCE
  from c45a452's own tools (`git archive c45a452 | tar -x -C <dir>`, then
  `python3 tests/test_triad_lane_tooling.py --capture <dir>`), because CI
  checks out at depth 1 where c45a452 is not there to run. It covers add
  (dry run and act), the sweep's porcelain and table with --branches
  --include-scratch --include-caches, lane-reconcile, lane-end refusing and
  passing with the row and last log line, lane-handoff's output, top block
  and inventory, and the estate report, on a repository with a submodule that
  is no leg and on one whose project.yaml names no leg but itself.
- Normalised for what no two runs share and nothing else: the bench's root
  and $HOME (both spellings), tree ids, operation ids, stamps, hashes, ages,
  byte sizes, the order of the inventory's sidecars (their file names are a
  checksum of the bench's path), and the sweep table's note about this
  account's unreadable /proc processes (a host fact). The golden test runs on
  Linux only, where it was captured.
- The fix since the run: `$HOME` is normalised to `<HOME>` before the root,
  so the fixture names no `/home/<user>/` path (the hygiene failure). Captured
  twice from c45a452 afterwards: byte-identical. The current tools' outputs
  on the same scenario equal it (compared by the module's own functions,
  not by pytest).

REMAINS: run `tests/run.sh -k "t018 or repo_hygiene"` and record the counts;
then (b) lane-handoff's paired-root poll and leg-checkout record with
lane-reconcile's leg registrations, (c) the report's legs, (d) add's leg
record (all three drafted in the session scratchpad, not applied), and the
shell suite once at the end.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP (untested): T010 docs — the §4.14 CI sketch exits 4 under #190's composed mode; three ways a composed job can pin the PR's code

Plan task T010 (#186). Queued by the coordinator from
the review of #190: the §4.14 sketch (code-PR CI with
OPENREPOTOOLS_COMPOSED=1 against an assembly checked out at main) would exit
4, because #190 at dbc6837 refuses any code root the assembly does not pin —
Roots.absent() (tests/conftest.py:283) runs _leg_at_its_pin (:259), and
pytest_configure (:404-412) raises it as pytest.UsageError — while
discover_assembly (:159-172) finds only an assembly that mounts the code
root. Dated notes in §4.14 and §7 item 7: the composed job must check out an
assembly whose code gitlink points at the PR's own commit, or a throwaway
assembly built for the run that pins it, or use #190's standalone mode.

Documentation only; no tool or test changes. Marked WIP (untested) because
the hygiene suite has not been run over this text: the checkpoint came first.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 (c): the daily report counts each leg of an assembly as a checkout of its own, with a legs section only where an assembly's leg is checked out

Plan task T018 (#186), proposal 3 of
docs/triad-lane-tooling-analysis.md §4.4. A leg is a submodule, its `.git` a
FILE, so `Report.build` never counted one as a canonical checkout and the
branches all lane work makes after the split never reached the report. Now,
for every canonical checkout whose own project.yaml declares legs (read by
`lanes-edit.sh project-legs`), each checked-out leg is read as a checkout of
its own: its branches ("Unmerged branches…"), its root `main` ("Root main
divergence") and its rescue branches ("Awaiting disposition"), each leg row
ending with `the <path> leg of <assembly>`; and a section "Assembly legs with
uncommitted or unpublished work" lists the legs holding dirty or untracked
paths or a HEAD no origin ref holds, or that could not be read. A manifest
that cannot be read is a row of "Records the report could not read". Guidance
in docs/README-lanes.md's report table and `lane-worktrees --help`.

THE INVARIANT: a canonical checkout with no project.yaml is asked nothing; the
legs section is added only where a leg was found, so a report of single
repositories is byte for byte what it was. This changes the daily report for
the estate's existing triads (IRRS, IRSS, MedxEHR, openDox), which is what
proposal 3 is for.

THE PROOF:
  * For 8e2227e and 96da9af, which were WIP: `tests/run.sh -k "t018 or
    repo_hygiene"` -> 162 passed, 1144 deselected in 134.24s (2026-10-09T13:25Z).
  * For this commit: run.sh's own in-lock command under the same lock, because
    the wrapper's workstation-wide census had waited 30 minutes on other
    projects' pytest runs (#184) - on the coordinator's word, the form lane 1
    approved on 2026-10-07: `flock -w 7200 /tmp/openrepotools-pytest.lock env
    TMPDIR=<run root>/tmp python3 -m pytest tests -q -p no:cacheprovider
    --basetemp=<run root>/basetemp -k "t018 or repo_hygiene or
    test_lane_worktrees_report"` -> 184 passed, 1124 deselected in 457.65s.
    That includes the single-repository golden (the report section of c45a452's
    stored outputs, unchanged), every test of tests/test_lane_worktrees_report.py,
    the hygiene module, and two new cases: an assembly whose code leg holds an
    unpushed branch and whose spec leg is dirty, and an estate with no assembly,
    which has no legs section.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 (d): lane-worktrees add records the leg it made a tree from, and says of a tree made from an assembly itself that its legs are empty there

Plan task T018 of #186, slice (d2), proposal 1 of
docs/triad-lane-tooling-analysis.md §4.4.

Where the lane's checkout is an ASSEMBLY - its project.yaml declares a leg
other than itself, read only through `lanes-edit.sh project-legs`, lane-start's
rung-5 reading - `lane-worktrees add` matches the checkout it makes the tree
from against each leg by their common git directory (the leg's own toplevel
must be the leg's path, so an uninitialized leg, which git answers as the
assembly, never matches). A tree made from the leg, or from any worktree of
that leg's repository (a paired <assembly>/worktrees/<feature>/code included),
is recorded in the #97 inventory with THE LEG as its checkout, which every
inventory reader enumerates; the dry-run and success notes name the leg, and
the success note says "with <leg> as its checkout" where the checkout given was
a worktree of it. A tree made from the assembly itself is made as before, and a
note says a worktree does not check the legs out and names the
`--checkout <assembly>/<leg>` that makes a leg's tree. A manifest that cannot
be read refuses, exit 2, nothing made (Amendment 7(d)).

THE INVARIANT: a lane whose checkout has no project.yaml is asked nothing (no
helper call), and a manifest that declares no leg other than `.` answers 8 -
so on a single repository `add`'s output, exit code, inventory row and
take-back text are byte-for-byte what they were. PROOF:
test_t018_single_repository_outputs_are_c45a452s_byte_for_byte compares the
current tools against tests/fixtures/t018/single-repository.golden, stored from
c45a452's tools, whose "add --dry-run", "add" and "add (manifest, no leg)"
sections are add's; it passes unchanged. The shell suite's
#163 add block (tests/test_lane_helpers.sh) runs once on the merged tree at the
end of this slice.

Help text that changes regardless of shape (listed in the §4.4 note for lane
1's ruling, item 2): `lane-worktrees --help`'s "A TRIAD" paragraph gains two
sentences on add. Guidance: docs/README-lanes.md "In a triad (T018), a tree of
a leg is made from the leg". The §4.4 note's first paragraph now names (a)
through (d), with (b) a second writer's branch this one merges.

Tests (tests/test_triad_lane_tooling.py, fixtures from openRepoShape's
assembly-root template bytes, legs behind bare local remotes, gh exits 1):
test_t018_add_records_the_leg_it_made_a_tree_from (dry run, an add from
<assembly>/code, an add from a paired worktree recorded under the leg, the
sweep then reading that tree through its leg) and
test_t018_add_from_the_assembly_itself_says_its_legs_are_empty.

COUNTS: `tests/run.sh -k "t018 or repo_hygiene or test_lane_worktrees_report"`
on this tree: 186 passed, 1124 deselected in 212.76s (0:03:32), exit 0
(30f9dc0's same selection: 184 passed; the two new (d) tests make 186).
run.sh's own census and lock (it waited ~48 minutes behind other runs;
no flock fallback was used). The selection holds no supervised-restart
test.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 (b): lane-handoff sees a triad's legs — the paired trees polled, each tree recorded under its leg

Plan task T018 (#186), slice (d2), part (b), first
half. Two gaps docs/triad-lane-tooling-analysis.md §4.4 and §4.7 measured in
the handoff's writer poll, closed SHAPE-GATED through 2aa8a59's shared reader
(`lanes-edit.sh project-legs`, called as it is):

(1) THE PAIRED ROOT IS POLLED. Where the lane's checkout is an assembly (its
    own project.yaml declares a leg other than itself), the handoff polls
    `<assembly>/worktrees/<feature>` and `<feature>/<each declared leg path>`
    as it polls `.claude/worktrees`: every directory there with a `.git` of its
    own (anything else under it is the assembly's working tree, where git
    answers too). A lane with unpushed work in a paired tree used to be absent
    from its WRITERS block.
(2) EACH TREE IS RECORDED UNDER ITS LEG. A polled tree whose repository's
    common git directory is a checked-out leg's is filed with that leg
    (project-legs' third column, the path `add --checkout` records) as its
    checkout. Anything else keeps the lane's dir, as before. The first handoff
    used to rewrite the leg that `lane-worktrees add --checkout <assembly>/code`
    recorded to the assembly. A leg that is NOT checked out is passed over:
    git answers the assembly there, and the drafts' leg map, which lacked that
    guard, would have filed every tree of the assembly under a deinitialized
    leg (measured: with the guard deleted, the deinit case fails).

AN UNREADABLE project.yaml is never "no legs" (Amendment 7(d)), and the swap
still completes (R-A11-11). The paired root is not polled, the top block's
WRITERS section carries a `- NOT POLLED:` line, no sidecar is filed for a
tree that is not of the lane's own repository (the checkout `add` recorded
is kept), and the lane stays SWAPPING.

THE GATE: a checkout with no project.yaml is asked nothing. There is no
helper call and no git read, and set-lane-tree gets the same argv. One whose
project.yaml names no leg but itself costs one `project-legs` call, which
answers 8, and nothing else. `--supervise` and `--restart-status` exit before
any changed line. No usage or --help text moves.

THE PROOF, under tests/run.sh, lanes-edit.sh at base (lane-reconcile is the
next commit):
  tests/run.sh -k "t018 or supervised_restart or repo_hygiene or openrepotools_command" -rs
  -> 7 failed, 429 passed, 874 deselected in 3149.31s (load 60-70 on 32 cores)
  The 429 include tests/test_triad_lane_tooling.py, whose single-repository
  golden from c45a452 passes UNREGENERATED (tests/fixtures/t018/ untouched),
  and the new tests/test_triad_lane_tooling_handoff.py: three assembly cases
  built from openRepoShape's template bytes at the pin (legs as real
  repositories behind bare local origins, fake gh exiting 1), and a second
  stored golden, tests/fixtures/t018-handoff/single-repository-decoys.golden.
  That golden was captured twice from c45a452's own tools, byte-identical,
  on a single repository laid out to look like a triad: a submodule's
  worktree under worktrees/<feature>/code, and a tree recorded with the
  submodule as its checkout. Its scenario has no project.yaml, and then one
  naming no leg but itself. The handoff output, top block, inventory and
  lane-reconcile before and after all match it.
  The 7 failures are --restart/--supervise timing cases of
  tests/test_supervised_restart_regressions.py (TimeoutExpired, "observer
  never confirmed readiness"). A/B at load ~40, one run.sh run each:
    this tree:       -k "<the 7>" -> 7 passed, 1303 deselected in 143.69s
    96da9af (archive copy, no change): -> 7 passed, 1299 deselected in 150.59s
  so they are load, not this change.
  Against 96da9af's lane-handoff the three new assembly cases fail
  (measured); the base handoff turns the `add`-recorded `<assembly>/code`
  into `<assembly>`.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 (b): lane-reconcile reads a triad's legs — each leg's worktree registrations and the paired root

Plan task T018 (#186), slice (d2), part (b), second
half: the third gap docs/triad-lane-tooling-analysis.md §4.4 measured.
`lanes-edit.sh lane-reconcile` read only the checkout's own worktree
registrations and the two lane roots, so a tree of a leg's repository that no
sidecar names was invisible to it. SHAPE-GATED, through `assembly_legs`
(2aa8a59's shared reader, called as it is):

- EVERY CHECKED-OUT LEG'S REGISTRATIONS, read beside the checkout's own. Each
  worktree git registers in a leg that no sidecar names is a TREE row:
  `unmanaged`, or `stale-registration` where its directory is gone. The row
  names the leg ("git registers it in <leg>, the code leg of
  <assembly>/project.yaml (<repository>)"). The leg's main entry, which git
  spells as its git directory under `.git/modules/`, is skipped by place. A
  leg that is not checked out (git answers the assembly there) registers
  nothing of its own and is passed over.
- THE PAIRED ROOT, after the lane roots: `<assembly>/worktrees/<feature>` and
  `<feature>/<each declared leg path>`, each holding a `.git` of its own that
  nothing above named (a lost registration, or a clone where a worktree
  belongs), is `unmanaged`.
- PATHS NOBODY READ ARE NOT CLEARED: a project.yaml that cannot be read, a
  leg whose `git worktree list` fails, or a paired root or feature directory
  that cannot be listed makes the verdict `indeterminate`. The unreadable
  manifest is said in the words `lane-worktrees`' sweep refuses with ("the
  legs of the assembly <dir> could not be read"), because the sweep reads
  this verdict first and refuses on it. The previous run's one failure,
  tests/test_triad_lane_tooling.py:616, was exactly that: same exit 2, the
  reconciliation's wording instead of the sweep's. One fact now has one
  spelling, and that test passes unchanged.

Nothing is adopted, deleted or overwritten, as before. THE GATE: a checkout
with no project.yaml is asked nothing. One whose project.yaml names no leg
but itself gets `assembly_legs`' 8 and nothing else. Every row, count and
verdict on a single repository is what it was.

THE PROOF, under tests/run.sh, this exact tree (lanes-edit.sh and the test
module last written 2026-10-10T02:43:15Z, the run started 02:43:34Z):
  tests/run.sh -k "t018 or supervised_restart or lanes_index or repo_hygiene or lane_start_claude_current or openrepotools_command" -rs
  -> 544 passed, 768 deselected in 1318.62s (0:21:58), on a quiet box
  The 544 include both T018 modules. Two goldens stored from c45a452's tools
  pass UNREGENERATED: tests/fixtures/t018/single-repository.golden (byte
  identical) and tests/fixtures/t018-handoff/single-repository-decoys.golden.
  The latter covers lane-reconcile before and after a handoff on a
  repository laid out to look like a triad. Also included are the
  supervised-restart module, the lanes-index module (the real
  lane-reconcile), lane-start's module (it calls lane-reconcile), hygiene
  and the installer module. Two new cases are in
  tests/test_triad_lane_tooling_handoff.py, on openRepoShape's template
  bytes at the pin: the legs' registrations and the paired root are
  reported (a spec tree made away from every root, a paired code worktree,
  a clone in the paired root, a stale registration; neither leg nor its
  `.git/modules` entry reported), and the indeterminate verdicts (unreadable
  manifest, unlistable paired root). Against 96da9af's lanes-edit.sh both
  fail (measured).
  The earlier run of this half, before the wording fix: 1 failed, 446
  passed, 865 deselected in 2718.97s (the :616 case above).

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP (untested): T018 (b) guidance — README-lanes says what lane-handoff and lane-reconcile read in a triad, writer 2's draft checked against 9a5bbe2

Writer 2's paragraph (briefs/t018-b-guidance.md, 2026-10-09T21:53Z) after
"`lane-end`'s gate" in the T018 section, each sentence checked against the
merged lane-handoff (4e39854) and lane_reconcile (9a5bbe2). Two corrections:
the unreadable-manifest verdict is spelled in the sweep's words, which the
sweep reads first and refuses on (exit 2); and a project.yaml naming no leg
but itself is asked one project-legs call (answering 8), not nothing.

Untested: committed for an account swap while the merged tree's scoped run
was in flight; that run's counts follow in the next commit.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP (untested): T018 (d) — add's refusal on an unreadable manifest is tested, and spelled as the sweep and lane-reconcile spell it

test_t018_add_refuses_where_the_manifest_cannot_be_read holds 13cb52d's README
claim (mode-000 project.yaml: exit 2, no tree, no inventory row). The refusal
reads "the legs of the assembly <dir> could not be read", the sweep's and
9a5bbe2's words, in place of "the legs of the lane's assembly <dir>". Reached
only where <lane dir>/project.yaml is there and project-legs fails on it; a
checkout with no project.yaml is asked nothing.

Untested: committed for an account swap while the merged tree's scoped run
was in flight; that run's counts follow in the next commit.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 docs: a parked paired tree is not the lane's to the sweep (§7 item 11, for lane 1's ruling), and §6 says what the branch now ships

docs/README-lanes.md, T018 section: a parked or resumed paired tree carries no
Lane: trailer (park.sh puts the lane in the WIP subject only; resume
soft-resets those commits), so the sweep and lane-end's gate count it for
nobody unless the lane's inventory names it. The paragraph says what to read
before lane-end in an assembly, and how to record such a tree with
set-lane-tree so the gate counts it.

docs/triad-lane-tooling-analysis.md: a dated §6 note on what the branch ships
beyond its first commit, and §7 item 11, the choice between reading park's
WIP subject and giving park's commit a trailer, plus whether resume records
the tree it rebuilds.

Written by the previous writer on 2026-10-11 and left uncommitted; checked
here against HEAD 8cb1368, whose lane-worktrees, lane-end and lane-handoff are
byte-identical to 9991d69's. Every line reference holds: lane-worktrees
:2424-2431, :4473-4494 and :2124, lane-end :2122-2127, the FOREIGN reason text
(lane-worktrees:3037-3039), and workBenches ffd5516 park.sh :226 and :860 and
resume.sh :221-231 and :746. Three corrections in item 11: the source is
review 5481327496 on #196 (a pull-request review, not an issue comment); its
finding 2 was derived from code and not run, so "measured" is now "derived",
the document's own word for that; and the inventory names a paired tree through
lanes-edit.sh set-lane-tree, not through lane-worktrees add, which makes its
trees under the lane's own root only.

Docs only. No test run for this commit; the branch's one verification run
follows the M1(a) and should-fix commits.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 (b), pre-PR review M1 option (a): lane-handoff polls the paired root by the sweep's rule, so one lane's handoff never takes another lane's feature trees; and S1, S5, N1-N5

M1 (a). In an assembly, lane-handoff polled and recorded every .git-bearing
tree under <assembly>/worktrees as the running lane's writer. The paired root
is per feature, not per lane, so after one handoff another lane's paired
work was this lane's inventory: lane-end refused on it, sweep --yes could
rescue and remove it, and the next session was told to relaunch writers into
it. Now a paired tree is polled and recorded only where this lane's inventory
names it (lanes-edit.sh lane-trees, read once), its HEAD's own last Lane:
trailer is this lane's on a commit origin's default branch lacks (a bash
mirror of lane-worktrees' own_trailers/head_lane_trailer; a clone is never
claimed by trailer, as the sweep never adopts one), or the --writers-file
names it. Every other checkout there is one "- PAIRED ROOT, not this lane's:
<path> — branch …" line under its own heading in the top block, outside
WRITERS, not counted and not recorded. Pending lane 1's ruling on #186
between (a) and (b).

S1. A paired root, feature directory or leg directory that cannot be listed
or searched was skipped in silence by the handoff. It is now a "- NOT
POLLED" line, a stderr note and a lane left SWAPPING, as is an inventory
that cannot be read. lane-reconcile (indeterminate) and the sweep's _paired
(exit 2) gain the one case they also passed over: a leg's directory under a
feature that cannot be searched, and in the sweep a root or feature
directory that can be listed but not searched.

N1. lane-reconcile passes the feature directory to awk through ENVIRON, not
-v. N3. The handoff's unreadable-manifest words are the sweep's and
lane-reconcile's ("the legs of the assembly <dir> could not be read").
N4. add's hint names every leg's --checkout, not legs[-1]. N5. The handoff
keeps a recorded checkout of the tree's own repository (a dependency nested
in a leg) instead of re-recording the tree under the assembly.

Tests. N2: the assembly cases wear conftest's NEEDS_UPSTREAM (composed mode
fails rather than skips), and the no-assembly report test needs no marker.
New: the two-lane M1 regression; S1's three unlistable directories (handoff
and reconcile); N5; S5(b) the sweep's absent row and add's toplevel guard on
a deinitialized leg; S5(c) an unreadable leg (counted, lane-end refuses),
the submodule-status refusal (unshadowed by reconcile) and _paired's own
refusals called directly; and the coordinator's quoted-manifest case
(openRepoShape v1.0.0, #395 writes path: "{{SPEC_PATH}}"): project-legs
already strips one pair of " or ' quotes, and the test holds it for both
quote styles. Two existing handoff tests change for M1 (a): the no-commit
paired spec tree is named by a writers file, and the paired code tree of the
deinitialized-leg case carries the lane's trailer.

A checkout with no project.yaml is untouched: every new path runs only where
project-legs answered legs (lane-handoff, lane-reconcile, the sweep) or the
manifest declares them (add).

Untested in this commit: the branch's one verification run follows on the
final tree, and its counts are in the commit that ends this step.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 docs: README-lanes and the analysis say what M1 (a), S1 and S2 changed; the analysis's false statements about the branch are dated and corrected (S4)

docs/README-lanes.md, T018 section:
- M1 (a): "each lane's handoff records every paired tree" is gone. The
  handoff polls and records a paired tree only where the lane's inventory
  names it, its HEAD's own Lane: trailer is the lane's on a commit origin's
  default branch lacks, or the writers file names it; every other one is a
  "PAIRED ROOT, not this lane's" line outside WRITERS, not counted or
  recorded. The sweep's bullet says the handoff no longer widens its
  paired-root rule, and the parked-tree paragraph says a handoff does not
  record a parked tree for the lane.
- S1: the handoff's NOT POLLED, stderr note and SWAPPING for an unlistable
  paired root, feature directory or leg directory, or an unreadable
  inventory; reconcile's indeterminate and the sweep's refusal now also
  cover a leg directory that cannot be searched.
- S2: "no --yes acts on a leg" is now "on a leg checkout": under --branches
  a leg's branches are rows like the checkout's own, and --yes deletes a
  merged lane branch there as it does in the checkout, bundling first.
- N5: a tree recorded under a checkout of its own repository (a dependency
  nested in a leg) keeps that checkout through a handoff.

docs/triad-lane-tooling-analysis.md (S4): dated 2026-10-13 notes at §0 item 1
and §6. "This branch carries no test change of its own" was true of its first
commit only; it now carries two test modules and two goldens and still
changes none of the suite's roots. §6 records M1 (a) and S1, and §7 item 11
says the handoff no longer closes the parked-tree gap by itself.

Verification, one run through tests/run.sh on this commit's tree, 1511f94
(4d4957d plus these docs; main f87fd5a merged at 8cb1368), 2026-10-13
19:10-19:40Z. Every module but the four below was --ignore'd, with
-k "triad_lane_tooling or repo_hygiene or test_the_lane_helper_suite_passes":
  181 passed, 1 failed, 1 deselected in 1818.96s.
  - tests/test_triad_lane_tooling.py, tests/test_triad_lane_tooling_handoff.py
    (both goldens unregenerated) and tests/test_repo_hygiene.py: all passed.
  - The shell suite, tests/test_lane_helpers_suite.py::
    test_the_lane_helper_suite_passes: 4598 passed, 2 failed. Both are
    lane-start row-stamp assertions, "…with clause (c)'s directory and
    window in the tail" and "the row's stamp states clause (c)'s two facts
    … in its TAIL". lane-start's cut_to_line caps the stamp at 230 characters
    (lane-start:3502-3510). Under run.sh the suite's $HOME is
    ~/.local/state/openRepoTools/tmp/<UTC>-<pid>/tmp/tmp.XXXXXXXXXX/home, and
    the stamp then reaches 231 characters before its window value, so the
    tail is cut ("…/home/projects/repoA; ..."). With a /tmp-length home it
    is 165. This branch changes none of lane-start, tests/test_lane_helpers.sh,
    tests/test_lane_helpers_suite.py or tests/run.sh (git diff f87fd5a is
    empty for all four), so the two failures do not come from it.
An earlier attempt (run1) ran nothing: zsh passed the --ignore list as one
argument and pytest exited 4.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP (untested): T018: lane-reconcile reports a leg whose .git git cannot read as the sweep does - a TREE row `unreadable` that requires recovery - instead of passing over it in silence

lanes-edit.sh, lane_reconcile, the legs' registrations: a declared leg
whose git toplevel is not the leg itself was skipped whether or not a
.git stood in it, so a leg with a .git of its own that git cannot read
(a gitfile naming no repository, or a .git where git answers the
assembly) said nothing, while the sweep's `leg` row for it is
`unreadable` and counted (exit 3, lane-end refuses). It now takes the
sweep's rule (probe_leg: toplevel not the leg, and a .git stands there)
and the sweep's word: one TREE row `unreadable`, in the sweep's words
("git could not be read through it (it has a .git of its own and git
could not read it: <git's answer>)"), counted under "require recovery",
so lane-start's recovery read counts it too. A leg with no .git (not
checked out) is passed over as before, and a path a sidecar already
names is not reported twice.

The verdict is not changed: the sweep counts that leg and goes on, and
an `indeterminate` here is what the sweep refuses on (exit 2) before it
reads the legs, which would hide its own count and break
test_t018_a_leg_git_cannot_read_is_counted_and_holds_lane_end.

tests/test_triad_lane_tooling_handoff.py: a triad whose spec leg's .git
is a gitfile naming no repository, then an empty .git directory: one
`unreadable` row naming the leg, 1 require recovery, the verdict
ungraceful-stop, and the sweep still exit 3 with the leg `unreadable`
and counted; readable again, and deinitialized, the leg names nothing.

docs/README-lanes.md (T018 section): one sentence for the row.

A checkout with no project.yaml is untouched: the block runs only where
project-legs answered legs (lrc_lgrc 0).

lane-handoff's own leg loop (lane-handoff:2093-2095) skips such a leg
in the same way; it is not changed here.

Untested: the one verification run (run3, on this tree 2f23887) waited in
the shared pytest lock queue from 20:50Z and was stopped at 21:21Z, before
it took the lock, for the account swap; no count was taken.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP (untested): T018: lane-handoff counts a leg git cannot read as unreadable, as the sweep and lane-reconcile do; verification not run

lane-handoff, the legs' common directories (was lines 2093-2095): a
declared leg whose git toplevel is not the leg itself was skipped whether
or not a .git stood in it. So a leg with a .git of its own that git
cannot read (a gitfile naming no repository, or a .git where git answers
the assembly) said nothing, while the sweep's `leg` row for it is
`unreadable` and counted and lane-reconcile's TREE row (9f23a6b) is
`unreadable` under "require recovery". Worse, a tree `lane-worktrees add
--checkout <leg>` made from that leg was re-recorded under the ASSEMBLY
at the handoff, over the checkout `add` recorded.

It now takes the sweep's rule (probe_leg: toplevel not the leg, and a
.git stands there) and the sweep's words: one `- UNREADABLE LEG:` line in
the top block's WRITERS section ("`<leg>` is unreadable, the <decl> leg
of <dir>/project.yaml (<repo>): git could not be read through it (it has
a .git of its own and git could not read it: <git's answer>)"), a note on
stderr, and a count (`legs: N of the assembly's legs under <dir>
unreadable`). As lane-reconcile keeps its verdict for it, the lifecycle is
not held SWAPPING for the leg itself: the poll looked everywhere it
looks. writer_checkout files no guessed checkout while a leg is
unreadable: a tree of no readable leg's repository, not of the
assembly's own and recorded under no checkout of its own repository is
named and NOT recorded, so the leg `add` recorded stays, and the lane
stays SWAPPING for that tree (the existing "no checkout can be vouched
for" rule). A leg with no .git (not checked out) is passed over as
before. A checkout with no project.yaml, or one naming no leg but
itself, never runs the leg loop: single-repository output is unchanged.

docs/README-lanes.md: the handoff paragraph says so.

tests/test_triad_lane_tooling_handoff.py:
test_t018_the_handoff_reports_a_leg_git_cannot_read_as_the_sweep_counts_it,
beside the reconcile one: the spec leg's .git a gitfile naming no
repository, then an empty .git directory: one UNREADABLE LEG line, the
note and the count, the lane SWAPPED, the reconciliation `unreadable`, 1
require recovery and `resumable`; a tree `add` made from the spec leg
keeps the spec leg as its checkout, named and not recorded, the lane
SWAPPING; readable again, nothing named and SWAPPED. The deinitialized
leg test now also holds that a leg with no .git is never `unreadable`.

VERIFICATION NOT RUN. The one run of the T018 selection (verify.sh, run4.log,
head 9f23a6b, index tree 0b691c8) waited for the shared pytest lock
behind other lanes' runs from 21:59:32Z and ended exit=143 (a SIGTERM
this writer did not send) at 22:26:46Z before any test ran; an account
swap was called at 22:20:05Z. So this commit and 9f23a6b
are both UNTESTED. Before this commit: bash -n lane-handoff is clean,
the test file parses. The next run on this tree verifies both.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018: lane-handoff withholds only a tree that could be an unreadable leg's, names a tree git cannot read, and says what it withheld (review of bf2a220, S1, S2, N1-N4)

S1. lane-handoff, writer_checkout: while a leg was unreadable, every
tree not of the assembly's own repository was withheld, a worktree of an
unrelated repository in the lane's own root included, so the lane stayed
SWAPPING on every handoff and those records went stale. Now a tree is
withheld only where it could be of that leg's repository: its common git
directory cannot be read, or lies physically inside the assembly or its
git directory (.git/modules/<leg>, a leg's own .git) without being the
assembly's own, or the inventory records it under an unreadable leg. Any
other tree keeps the lane's dir, as when every leg reads.
Test: test_t018_an_unreadable_leg_withholds_no_tree_of_another_repository
(a worktree of a third repository, recorded before and made after the
leg broke; and one a record names under the unreadable leg).

S2. lane-handoff, poll_writer: `git rev-parse --git-dir || return 0`
dropped any tree whose .git git cannot read, so a damaged
.git/modules/<leg> took every tree of that leg out of WRITERS and the
count while the lane went SWAPPED. In an assembly (legs_state set) such
a tree is now listed and counted, UNREADABLE and (NOT recorded), added to
lc_inventory_missed, and named on stderr. A checkout with no project.yaml
naming a leg passes over it as before, so the goldens do not move.
Test: test_t018_a_tree_git_cannot_read_in_an_assembly_is_named_and_holds_the_lane_swapping
(modules/spec deleted after add --checkout spec, with a paired spec tree
the inventory names; and a single repository's tree, passed over).

N1. The UNREADABLE LEG line's clause now depends on whether any tree was
withheld ("the N tree(s) this handoff could not place are named above,
each marked (NOT recorded)", or "every tree this handoff polled could
still be placed, so none is withheld for it"), and a withheld tree's
WRITERS line is marked (NOT recorded). The leg rows are kept as
<leg> TAB <why> and written by top_block after the poll.
N2. lane-handoff's UNREADABLE LEG line and note, and lanes-edit.sh's
lane-reconcile TREE row for an unreadable leg, say "before relaunching
a writer onto a tree of its repository": a leg is the assembly's.
N3. docs/README-lanes.md says the leg count is stderr's `legs: N ...`
line, never WRITERS' (N found), and describes S1, S2 and N1.
N4. Test: test_t018_a_leg_tree_add_made_is_withheld_where_the_legs_git_is_an_empty_directory
(the empty .git directory, where git answers the assembly in the leg,
with a tree add made from it).
The existing handoff and reconcile unreadable-leg tests now also assert
N1 and N2.

bash -n passes on lane-handoff and lanes-edit.sh; the added lines use no
declare -A, ${x,,}, mapfile, readlink -f, <<< or [[.

Authoring smoke check only (the one verification follows on the final
head): the three new tests, the two changed ones and the handoff golden
test, each called once outside pytest with LANES_* and TMUX* unset, all
passed; the three new tests each failed against cf7db4a's tools, on the
withheld third-repository tree, the writer count and the (NOT recorded)
marker respectively.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 docs: record lane 1's rulings of comment 6240800023 in the analysis's §7 (items 3, 11 and a new 12 for M1)

docs/triad-lane-tooling-analysis.md §7: a dated head note naming
comment 6240800023 on #186 (posted 2026-10-17T20:48Z), and each ruling
at its item:
- item 3: proposal 1 (the guidance) goes live now; proposals 2 and 3 go
  live with the T018 PR, on the condition of the read-only before/after
  measurement over the triads (IRRS, IRSS, MedxEHR, MedxGlass, openDox,
  openXdox, openWallet and the codexFactory copies), main's tools against
  T018's, and any verdict that flips to a refusal told to its owning
  lane before the landing;
- item 11: (ii), park's WIP commit carries a `Lane: <lane>` trailer, and
  resume records the tree it rebuilds with `lanes-edit.sh set-lane-tree
  <lane> <path> --checkout <assembly>/<leg>`; both workBenches changes,
  one workBenches issue linked from the T018 PR ("workBenches issue: to
  be linked"); the PR does not wait for it;
- item 12, new: the pre-PR review's M1, (a) as implemented in 4d4957d,
  with the two-lane regression test
  (test_t018_a_handoff_takes_no_other_lanes_paired_tree).
§6's 2026-10-13 note now says the M1 ruling was made, and where.

docs/README-lanes.md: the two T018 sentences that called these rulings
pending now say they were made, and the parked-tree check before
lane-end stands until the workBenches changes land.

Docs only.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018: the daily report lists a leg that is also a canonical clone once (measurement finding 1)

lane-worktrees, the report's _branches, _root_main and _aging iterated
`canonical + [g.path for g in legs if g.checkout]`. A leg whose .git is
a directory (a clone, not a submodule's gitfile) is also canonical, so
it was read twice: the read-only triad measurement of 2026-10-17
(finding 1) saw 8 branch rows printed twice on this workstation, and
_root_main would do the same for a leg whose main is ahead. _aging's
`seen` set hid it there only where the two spellings matched.

All three now read Report._checkouts(canonical): the canonical
checkouts, then the checked-out legs, each once by os.path.realpath,
first occurrence kept. With no leg checked out it returns canonical
untouched, so a single-repository report cannot move. _leg_cell also
matches a leg by real path, so the row kept (the canonical spelling)
still names its leg.

Test: test_t018_the_report_lists_a_leg_that_is_also_a_canonical_clone_once
(an assembly whose project.yaml declares two legs that are clones: the
code leg's unpushed branch, its aged rescue branch and the spec leg's
main ahead of origin each appear once, each naming its leg). It needs
no template. An authoring smoke check outside pytest: it passes on this
tree and fails on 69f0017's lane-worktrees, on the duplicated branch row.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 docs: the analysis spells bench paths ~/projects/..., as the hygiene rule of #229 item 22 asks

f595e44 made tests/test_repo_hygiene.py refuse a committed path that
starts at the bench root, except inside a single-quoted grep pattern.
docs/triad-lane-tooling-analysis.md had seven such lines (§0, §4.2,
§4.5, §4.9, §4.12 and two rows of §4.15). Each now says ~/projects/...,
the bench's projects directory, as f595e44 respelled the same path in
ideation/; the §4.15 grep pattern keeps the spelling it searched for,
under the rule's allowance. A dated note under the header says so.
host_absolute_path_hits over every file this branch adds or changes
now finds nothing.

Docs only.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018: verify 88ee9d9: 224 passed, 1 failed (test_the_lane_helper_suite_passes: two #180 row-stamp cuts, main's own); selection triad_lane_tooling or repo_hygiene or test_the_lane_helper_suite_passes

Log: briefs/t018-verify-20261018T034203Z.log (lane openRepoTools-3's
briefs directory), through briefs/verify.sh and tests/run.sh, with every
LANES_* and TMUX* variable unset.
  start 2026-10-18T03:42:03Z head 88ee9d9 tree-of-index 07fa62e
  1 failed, 224 passed, 1 deselected in 4045.64s (1:07:25)
  exit=1 end 2026-10-18T05:34:13Z
The run queued behind other lanes' pytest from 03:42Z and took the
lock at about 04:26Z.

Every test of tests/test_triad_lane_tooling.py,
tests/test_triad_lane_tooling_handoff.py and tests/test_repo_hygiene.py
passed, both goldens unregenerated among them.

The one failure, tests/test_lane_helpers_suite.py::
test_the_lane_helper_suite_passes: the shell suite ended "4620 passed,
2 failed, 0 skipped, 0 pending". Both FAILs are lane-start row-stamp
tail cases of #180's group:
- log line 100, "...with clause (c)'s directory and window in the
  tail, for the person reading the row" (repoA): the cell line is cut
  to "...home/projects/repoA; ..." (228 chars), so "; window " is gone;
- log line 1967, "...having found the checkout by its real directory
  name, not the typed one" (repoCase): the cell line is cut to
  "...no launch; dir ..." (123 chars), the whole path dropped.
Cause: lane-start's cut_to_line caps the state line at 230 characters,
cuts there and trims back to the last space, then appends " ...". The
"; dir <DIR>; window <ref>" tail comes last, and here <DIR> is about 110
characters (the run root under ~/.local/state/openRepoTools/tmp/...), so
the tail is cut: in repoA after the path, in repoCase inside it, where
trimming to the last space drops the path whole. Both "got" strings are
exactly that cut.
Not T018's: `git diff origin/main...HEAD -- lane-start
tests/test_lane_helpers.sh tests/test_lane_helpers_suite.py` is empty,
and T018's lanes-edit.sh hunks (usage, project_legs/assembly_legs,
lane_reconcile, a comment on lane_worktree_roots, the guard exemption,
the project-legs arm and the refusal list) touch neither set-row-state
nor the stamp. Lane 2 measured the same group failing on main's own tree
at a TMPDIR length of 86 (#186 comment 6244714538: repoA, repoA11b,
repoCase and repoMix); #215 (61a7378) moved only repoA11b to a short
fixed path, and repoA11b passed here. CI's tests job, with a short run
root, passes this test on main (83d2690: 1498 passed, 19 skipped;
f5c3bcd and 2cb14a9: success). Not re-run on main locally: the two cases
run only inside the hour-long shell suite.

This run covers 9f23a6b and bf2a220 (the unreadable-leg reads) and this
round: the S1, S2 and N1-N4 fixes (5d2c1c8), lane 1's rulings in §7
(9a8f175), measurement finding 1 (a4e0b17), the bench-root respelling
(a60cf1c), and the merges of main 2732d59, 83d2690, f595e44 (the
openRepoShape pin at v1.1.0) and f5c3bcd (cf7db4a, 107958c, 69f0017,
88ee9d9). The earlier run on 107958c (t018-verify-20261018T011535Z.log)
was killed at a session exit after one F (first in order:
test_the_lane_helper_suite_passes) and 176 passes, with no summary.

Correction to a60cf1c's message: the first of the seven respelled
lines is in §1 (Scope and method), not §0.

Main has since moved to 2cb14a9 and a41adb6; this head does not
include them.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* T018 tests: the unreadable-leg tests ask the git under test what it says, instead of expecting git 2.43's wording (PR #246 CI)

PR #246's Linux tests job (run 39113292535, job 117633782691) failed
two tests: test_t018_reconcile_reports_a_leg_git_cannot_read_as_the_
sweep_counts_it and test_t018_the_handoff_reports_a_leg_git_cannot_
read_as_the_sweep_counts_it. Both wrote a spec leg's .git as
`gitdir: /nonexistent/t018-spec` and expected git's quoted answer to
contain that path. Git words that condition by version:
- 2.43 (this bench): fatal: not a git repository: /nonexistent/t018-spec
- 2.54 (alpine/git:latest, a throwaway container): fatal: not a git
  repository (or any parent up to mount point /)
- 2.55 (CI's runner): fatal: not a git repository: (null)
On CI the tools still classified the leg `unreadable`, counted it and
named it: lane-handoff's stderr shows "the leg .../triad/spec is
unreadable: ... git could not read it: fatal: not a git repository:
(null)". The code reads the condition (git's toplevel there is not the
leg, and a .git stands in it) and only quotes git's line, so the
code is right and the tests were wrong.

tests/test_triad_lane_tooling_handoff.py: a new helper, git_says(b,
leg), returns what every reader quotes (probe_leg's _first_line,
lane-reconcile's and lane-handoff's first non-empty line, or "git
answers <toplevel> there"), asked of the git under test. The three
gitfile checks use it. The empty-.git-directory checks keep "git
answers <assembly> there", the tools' own words, which passed on CI.
No tool changes; S1 and S2 are untouched.

Before this commit: under a PATH shim that rewrites only the
/nonexistent error line to 2.55's "(null)" form, both tests passed
when called directly outside pytest (13 s and 37 s).

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re-run CI on main fe58808: refs/pull/246/merge was stale on 14239b9

No content change: this commit's tree is ea4db0d's. Lane openRepoTools-1, the lander, pushed it on lane openRepoTools-3's word in #186 6250521287, so the landing gate's Linux run tests the merge with current main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brett Heap <service@opensoft.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready landing gate: runs tests-macos once before the squash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tests/run.sh: a long run root truncates the row stamp so test_lane_helpers.sh's clause (c) TAIL case fails locally while CI's plain pytest passes

1 participant